ListView 与 RecyclerView 缓存对比

Posted by LinYaoTian on 2019-02-03

相关知识:

ListView 和 RecyclerView 的缓存机制原理大致相似,如下图所示:

image

过程中,离屏的ItemView即被回收至缓存,入屏的ItemView则会优先从缓存中获取,只是ListView与RecyclerView的实现细节有差异。(这只是缓存使用的其中一个场景,还有如刷新等)

缓存机制对比

1.层级不同

RecyclerView 比 ListView 多了两层缓存,支持多个离屏 ItemView 的缓存,支持开发者自定义缓存处理逻辑,支持所有 RecyclerView 共用一个 RecyclerViewPool 缓存池。

具体来说:

ListView(两级缓存):

image

RecyclerView(四级缓存):

image

ListView 和 RecyclerView 缓存机制基本一致:

1). mActiveViews 和 mAttachedScrap 功能相似,意义在于快速重用屏幕上可见的列表项 ItemView,而不需要重新 createView和 bindView。

2). mScrapView 和 mCachedViews + mReyclerViewPool 功能相似,意义在于缓存离开屏幕的 ItemView,目的是让即将进入屏幕的 ItemView 重用。

3). RecyclerView 的优势在于:

  • a.mCacheViews 的使用,可以做到屏幕外的列表项 ItemView 进入屏幕内时也无须 bindView 快速重用;
  • b.mRecyclerPool 可以供多个 RecyclerView 共同使用,在特定场景下,如 viewpaper + 多个列表页下有优势,客观来说,RecyclerView 在特定场景下对 ListView 的缓存机制做了补充和完善。

2.缓存的对象不同

1). RecyclerView 缓存 ViewHolder,抽象可理解为:View + ViewHolder(避免每次 createView 时调用 findViewById) + flag(标识状态);
2). ListView 直接缓存 View。

缓存不同,二者在缓存的使用上也略有差别,具体来说:

ListView获取缓存的流程:

image

RecyclerView获取缓存的流程:

image

1).RecyclerView 中 mCacheViews(屏幕外)获取缓存时,是通过匹配 pos 获取目标位置的缓存,这样做的好处是,当数据源数据不变的情况下,无须重新 bindView。

情景:用手指上下滑动,重复让 2-3 个 Item 滑出和滑入屏幕,此时是不需要重新 bindView 的。如果一直向一个方向滑动,RecyclerView 还是会去 bindView,因为 mCacheViews 缓存空间有限。

而同样是离屏缓存,ListView 从 mScrapViews 根据 pos 获取相应的缓存,但是并没有直接使用,而是重新 getView(即必定会重新 bindView),相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
//通过匹配pos从mScrapView中获取缓存
final View scrapView = mRecycler.getScrapView(position);
//无论是否成功都直接调用getView,导致必定会调用createView
final View child = mAdapter.getView(position, scrapView, this);
if (scrapView != null) {
if (child != scrapView) {
mRecycler.addScrapView(scrapView, position);
} else {
...
}
}

2).ListView 中通过 pos 获取的是 view,即 pos → view。
RecyclerView 中通过 pos 获取的是 viewholder,即 pos → (view,viewHolder,flag);
从流程图中可以看出,标志 flag 的作用是判断 view 是否需要重新 bindView,这也是 RecyclerView 实现局部刷新的一个核心。

局部刷新

由上文可知,RecyclerView 的缓存机制确实更加完善,但还不算质的变化,RecyclerView 更大的亮点在于提供了局部刷新的接口,通过局部刷新,就能避免调用许多无用的 bindView。

[RecyclerView和ListView添加,移除Item效果对比]

image

结合 RecyclerView 的缓存机制,看看局部刷新是如何实现的:以 RecyclerView 中 notifyItemRemoved(1) 为例,最终会调用 requestLayout(),使整个 RecyclerView 重新绘制,过程为:

onMeasure()→onLayout()→onDraw()

其中,onLayout() 为重点,分为三步:

  1. dispatchLayoutStep1():记录 RecyclerView 刷新前列表项 ItemView 的各种信息,如 Top、Left、Bottom、Right,用于动画的相关计算;
  2. dispatchLayoutStep2():真正测量布局大小,位置,核心函数为 layoutChildren()
  3. dispatchLayoutStep3():计算布局前后各个 ItemView 的状态,如Remove、Add、Move、Update等,如有必要执行相应的动画。

其中,layoutChildren()流程图:

image

image

当调用 notifyItemRemoved() 时,会对屏幕内 ItemView 做预处理,修改 ItemView 相应的 pos 以及 flag (流程图中红色部分):

img

当调用fill()RecyclerView.getViewForPosition(pos) 时,RecyclerView 通过对 pos 和 flag 的预处理,使得bindview()只调用一次.

需要指出,ListView 和RecyclerView最大的区别在于数据源改变时的缓存的处理逻辑,ListView是”一锅端”,将所有的 View 都重新 bindView(),而 RecyclerView 则是更加灵活地对每个 View 修改标志位,区分是否重新bindView()

参考

Bugly 的 Android ListView 与 RecyclerView 对比浅析–缓存机制